1 module hip.util.path; 2 import hip.util.string; 3 import hip.util.system; 4 //Node required for buildFolderTree 5 public import hip.util.data_structures: Node; 6 7 version(Windows) 8 enum defaultCaseSensitivity = false; 9 else version(Darwin) 10 enum defaultCaseSensitivity = false; 11 else// version(Posix) 12 enum defaultCaseSensitivity = true; 13 14 version(Windows) 15 { 16 enum pathSeparator = '\\'; 17 enum otherSeparator = '/'; 18 } 19 else 20 { 21 enum pathSeparator = '/'; 22 enum otherSeparator = '\\'; 23 } 24 25 string[] pathSplitter(string path) @safe pure nothrow 26 { 27 string[] ret; 28 foreach(p; pathSplitterRange(path)) 29 ret~= p; 30 return ret; 31 } 32 33 auto pathSplitterRange(string path) pure @safe nothrow @nogc 34 { 35 struct PathRange 36 { 37 string path; 38 size_t indexRight = 0; 39 40 bool empty() @safe pure nothrow @nogc {return indexRight >= path.length;} 41 string front() @safe pure nothrow @nogc 42 { 43 size_t i = indexRight; 44 while(i < path.length && path[i] != '\\' && path[i] != '/') 45 i++; 46 indexRight = i; 47 return path[0..indexRight]; 48 } 49 void popFront() @safe pure nothrow @nogc 50 { 51 if(indexRight+1 < path.length) 52 { 53 path = path[indexRight+1..$]; 54 indexRight = 0; 55 } 56 else 57 indexRight+= 1; //Guarantees empty 58 } 59 } 60 61 return PathRange(path); 62 } 63 64 bool isRootOf(string theRoot, string ofWhat) pure nothrow @nogc 65 { 66 auto pathA = pathSplitterRange(theRoot); 67 auto pathB = pathSplitterRange(ofWhat); 68 69 for(; !pathA.empty && !pathB.empty; pathA.popFront, pathB.popFront) 70 { 71 string compA = pathA.front; 72 string compB = pathB.front; 73 if(compA != compB) 74 return false; 75 } 76 return true; 77 } 78 79 80 81 string relativePath(bool caseSensitive = defaultCaseSensitivity)(string filePath, string base) pure nothrow @safe 82 { 83 int commonIndex = 0; 84 bool isEqual = true; 85 for(int i = 0; i < base.length; i++) 86 { 87 if(i == filePath.length || (caseSensitive ? base[i] != filePath[i] : base[i].toLowerCase != filePath[i].toLowerCase)) 88 { 89 isEqual = false; 90 break; 91 } 92 else if(base[i] == pathSeparator) 93 commonIndex = cast(int)i; 94 } 95 if(isEqual) 96 { 97 if(filePath.length == base.length) 98 return "."; 99 else //If the base string is a subset, return part after base. 100 return filePath[base.length + (filePath[base.length] == pathSeparator ? 1 : 0)..$]; 101 } 102 else if(commonIndex == 0) 103 return filePath; 104 105 string ret; 106 for(uint i = commonIndex; i < base.length; i++) 107 { 108 if(base[i] == pathSeparator) 109 ret~= ".."~pathSeparator; 110 } 111 ret~= filePath[commonIndex] == pathSeparator ? filePath[commonIndex+1..$] : filePath[commonIndex..$]; 112 return ret; 113 } 114 115 bool isAbsolutePath(string fPath) pure nothrow @nogc @safe 116 { 117 if(fPath == null) 118 return false; 119 version(Posix) 120 if(fPath[0] != '/') 121 return false; 122 version(Windows) 123 { 124 if(fPath.length < 3) 125 return false; 126 if(!(fPath[0].isUpperCase && fPath[1] == ':' && fPath[2] == '\\')) 127 return false; 128 } 129 for(size_t i = 0; i < fPath.length; i++) 130 if(i + 2 < fPath.length && fPath[i] == '.' && fPath[i+1] == '.' && fPath[i+2] == pathSeparator) 131 return false; 132 return true; 133 } 134 135 136 137 char determineSeparator (string filePath) pure nothrow @nogc @safe 138 { 139 size_t i = 0; 140 while(i < filePath.length && filePath[i] != '/' && filePath[i] != '\\') 141 i++; 142 return i < filePath.length ? filePath[i] : '\0'; 143 } 144 145 ///Will get the directory name until a trailing separator or return 146 string dirName(string filePath) pure nothrow @nogc @safe 147 { 148 char sep = determineSeparator(filePath); 149 if(sep == '\0') 150 return filePath; 151 int last = filePath.lastIndexOf(sep); 152 if(last == -1) 153 return filePath; 154 return filePath[0..last]; 155 } 156 157 158 string filename(string filePath) @safe pure nothrow @nogc 159 { 160 char sep = determineSeparator(filePath); 161 if(sep == '\0') 162 return filePath; 163 int last = filePath.lastIndexOf(sep); 164 if(last == -1) 165 return filePath; 166 return filePath[last+1..$]; 167 } 168 169 alias baseName = filename; 170 171 ref string filename(return ref string filePath, string newFileName) @safe pure nothrow 172 { 173 return filePath = replaceFileName(filePath, newFileName); 174 } 175 176 string filenameNoExt(string filePath) @safe pure nothrow @nogc 177 { 178 string f = filePath.filename; 179 if(f == "") 180 return ""; 181 int last = f.lastIndexOf("."); 182 if(last == -1) 183 return f; 184 return f[0..last]; 185 } 186 187 string replaceFileName(string filePath, string newFileName) @safe pure nothrow 188 { 189 char sep = determineSeparator(filePath); 190 string[] p = pathSplitter(filePath); 191 p[$-1] = newFileName; 192 return ((p[0] == "" && sep == '/') ? "/" : "") ~ joinPath(sep, p); 193 } 194 195 string normalizePath(string path) 196 { 197 string[] normalized; 198 foreach(p; pathSplitterRange(path)) 199 { 200 if(p == ".") 201 continue; 202 else if(p == "..") 203 { 204 if(normalized.length > 0) 205 normalized = normalized[0..$-1]; 206 else 207 normalized~= p; 208 } 209 else 210 normalized~= p; 211 212 } 213 return normalized.joinPath; 214 } 215 216 217 218 /** 219 * Extension getter 220 ```d 221 string myFile = "test.png"; 222 writeln(myFile.extension); //png 223 ``` 224 */ 225 string extension(string pathOrFilename) pure nothrow @nogc @safe 226 { 227 auto ind = pathOrFilename.lastIndexOf("."); 228 if(ind == -1) 229 return ""; 230 return pathOrFilename[cast(uint)ind+1..$]; 231 } 232 233 /** 234 * Extension setter. 235 * Usage: 236 ```d 237 string test = "test.png" 238 test.extension = "txt"; 239 writeln(test); //test.txt 240 ``` 241 */ 242 ref string extension(return ref string pathOrFilename, string newExt) 243 { 244 auto ind = pathOrFilename.lastIndexOf("."); 245 if(ind != -1 && ind != pathOrFilename.length) 246 { 247 if(newExt.length == 0) 248 pathOrFilename = pathOrFilename[0..ind]; 249 else if(newExt[0] != '.') 250 pathOrFilename = pathOrFilename[0..ind+1]~newExt; 251 else 252 pathOrFilename = pathOrFilename[0..ind+1]~newExt[1..$]; 253 } 254 return pathOrFilename; 255 } 256 257 string extension(string pathOrFilename, string newExt) 258 { 259 pathOrFilename = pathOrFilename.extension(newExt); 260 return pathOrFilename; 261 } 262 263 string joinPath(char separator, in string[] paths ...) @safe pure nothrow 264 { 265 if(paths.length == 1) 266 return paths[0]; 267 string output; 268 for(int i = 0; i < paths.length; i++) 269 { 270 string filePath = paths[i]; 271 string next = i+1 < paths.length ? paths[i+1] : ""; 272 273 if(filePath == "") 274 continue; 275 276 output~=paths[i]; 277 if(next != "" && next[0] != separator && 278 paths[i][$-1] != separator) 279 output~=separator; 280 } 281 return output; 282 } 283 284 string joinPath(in string[] paths ...) @safe pure nothrow 285 { 286 char sep; 287 foreach(p; paths) 288 { 289 sep = determineSeparator(p); 290 if(sep != '\0') 291 break; 292 } 293 if(sep == '\0') 294 sep = pathSeparator; 295 return joinPath(sep, paths); 296 } 297 298 299 public Node!string buildFolderTree(string[] filesList) 300 { 301 alias DirNode = Node!string; 302 DirNode root = new DirNode(filesList[0]); 303 304 scope DirNode[] dirStack = [root]; 305 306 for(size_t i = 1; i < filesList.length; i++) 307 { 308 int currStack = 0; 309 foreach(pathPart; pathSplitterRange(filesList[i])) 310 { 311 if(pathPart.extension != "") //It is a leaf if it has an extension 312 { 313 dirStack[$-1].addChild(pathPart); 314 } 315 else if(currStack >= dirStack.length) //If we have more parts than the stack has children, add to the stack 316 { 317 //Add child to the last 318 dirStack~= dirStack[$-1].addChild(pathPart); 319 currStack++; 320 } 321 else if(dirStack[currStack].data != pathPart) //If both they are the same, check for the next part 322 { 323 dirStack = dirStack[0..$-1]; 324 currStack--; 325 } 326 else if(dirStack[currStack].data == pathPart) //If both they are the same, check for the next part 327 currStack++; 328 } 329 } 330 return root; 331 } 332 string buildPath(Node!string node) 333 { 334 string ret = node.data; 335 while(node.parent !is null) 336 { 337 node = node.parent; 338 if(node) 339 { 340 ret = node.data~"/"~ret; 341 } 342 } 343 return ret; 344 } 345 346 347 ///Copied from dmd. 348 unittest 349 { 350 assert(baseName("a/b/test.txt") == "test.txt"); 351 assert(relativePath("foo", "") == "foo"); 352 assert(filenameNoExt("helloWorld.zip") == "helloWorld"); 353 assert("/hello/test/again".isRootOf("/hello/test/again/something/is/here.txt")); 354 355 version (Posix) 356 { 357 assert(filename("/something/here/yet.txt"), "yet.txt"); 358 assert(filenameNoExt("/something/here/yet.txt"), "yet"); 359 360 assert(relativePath("foo", "/bar") == "foo"); 361 assert(relativePath("/foo/bar", "/foo/bar") == "."); 362 assert(relativePath("/foo/bar", "/foo/baz") == "../bar"); 363 assert(relativePath("/foo/bar/baz", "/foo/woo/wee") == "../../bar/baz"); 364 assert(relativePath("/foo/bar/baz", "/foo/bar") == "baz"); 365 } 366 version (Windows) 367 { 368 assert(filename(`c:\something\here\yet.txt`), "yet.txt"); 369 assert(filenameNoExt(`c:\something\here\yet.txt`) == "yet"); 370 371 assert(relativePath("foo", `c:\bar`) == "foo"); 372 assert(relativePath(`c:\foo\bar`, `c:\foo\bar`) == "."); 373 assert(relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`); 374 assert(relativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`) == `..\..\bar\baz`); 375 assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz"); 376 assert(relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`); 377 } 378 }